Explaining MVVM, Repository Patterns, and Dependency Injection
Course: Android App Development
Topic: Explaining MVVM, Repository Patterns, and Dependency Injection
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction to Android Application Architecture
Android application architecture refers to the way an application organizes its code, user interface, business logic, and data operations. A well-designed architecture makes an application easier to develop, test, maintain, and extend.
As an Android application grows, placing UI code, API calls, database operations, and business rules inside a single Activity becomes difficult to manage. It can also create tightly coupled components that are hard to test independently.
Three important concepts used to improve Android application architecture are:
- MVVM: Model-View-ViewModel, an architectural pattern that separates UI rendering from UI-related state and logic.
- Repository Pattern: An abstraction that separates data access from the rest of the application.
- Dependency Injection (DI): A technique for supplying an object with the dependencies it needs instead of having it create those dependencies itself.
These concepts work well together. MVVM organizes presentation logic, repositories coordinate data access, and dependency injection provides the required objects.
2. Learning Objectives
After completing this topic, learners should be able to:
- Explain the purpose of application architecture in Android development.
- Understand the responsibilities of Model, View, and ViewModel.
- Implement MVVM using Kotlin and Android Jetpack components.
- Understand how a repository separates data access from UI logic.
- Work with repositories that use network and local data sources.
- Explain constructor injection and dependency injection.
- Understand dependency injection using Hilt.
- Connect a ViewModel, repository, and data source.
- Write testable and maintainable Android code.
- Identify common architecture mistakes and apply best practices.
3. What Is MVVM?
MVVM stands for Model-View-ViewModel. It is an architectural pattern that separates the presentation layer into components with distinct responsibilities.
MVVM helps prevent UI components from becoming overloaded with business logic, data processing, and database operations.
3.1 Components of MVVM
| Component |
Responsibility |
Android Example |
| Model |
Represents application data and domain operations. |
Data classes, domain models, repositories, and data sources. |
| View |
Displays data and forwards user interactions. |
Activity, Fragment, or Jetpack Compose screen. |
| ViewModel |
Manages UI-related state and coordinates presentation logic. |
A class extending AndroidX ViewModel. |
3.2 How MVVM Works
User interacts with the screen
|
v
View
|
v
ViewModel
|
v
Repository
|
v
Data Source / API / DB
|
v
Repository
|
v
ViewModel
|
v
Updated UI State
|
v
View
For example, when a user opens a product screen, the View requests the product information through the ViewModel. The ViewModel calls the repository, which obtains the data from an appropriate source. The ViewModel then exposes the resulting UI state, and the View renders it.
4. Understanding the Model in MVVM
The Model represents the data and domain concepts used by the application. In Android projects, this may include data classes, business rules, repository interfaces, and data-layer implementations.
A model should describe the information the application works with rather than directly controlling how a screen looks.
Example: Product Model
data class Product(
val id: Int,
val name: String,
val price: Double
)
This data class represents a product with an identifier, name, and price. It can be used by repositories, ViewModels, and UI components.
Responsibilities of the Model
- Represent application entities.
- Define domain-specific data structures.
- Support business rules where appropriate.
- Provide data to other architectural layers through defined interfaces.
- Avoid depending directly on Activity or Fragment UI elements.
5. Understanding the View in MVVM
The View is responsible for displaying information and capturing user interactions. In Android, a View may be an Activity, Fragment, or Jetpack Compose screen.
The View should not directly contain complex database queries or extensive business logic. Instead, it observes or collects UI state from the ViewModel and forwards user actions to it.
Responsibilities of the View
- Display data received from the ViewModel.
- Show loading indicators, errors, and empty states.
- Capture clicks, text input, and other user actions.
- Forward user actions to the ViewModel.
- Respect the UI lifecycle when observing state.
For example, a login screen collects email and password input, invokes the appropriate ViewModel function, and displays the current loading or error state.
6. Understanding the ViewModel
The ViewModel manages UI-related state and presentation logic. It acts as a bridge between the View and the data or domain layer without holding direct references to short-lived UI objects such as an Activity or Fragment.
AndroidX ViewModel is designed to survive configuration changes such as screen rotation when the same ViewModel owner is retained. It does not automatically persist all state across process death.
Responsibilities of the ViewModel
- Expose data in a form suitable for UI rendering.
- Coordinate user actions and repository calls.
- Manage loading, success, empty, and error states.
- Transform application data into presentation-ready information.
- Launch asynchronous work using lifecycle-aware coroutine scopes when appropriate.
- Avoid direct references to Activities, Fragments, or Views.
Example: Simple ViewModel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> =
_count.asStateFlow()
fun increment() {
_count.value += 1
}
}
In this example, the ViewModel owns the counter state. The private mutable StateFlow allows the ViewModel to update the value, while the public StateFlow allows the UI to observe it without directly modifying it.
7. What Is the Repository Pattern?
The Repository Pattern provides a central abstraction for retrieving, storing, and managing application data. It separates the code that requests data from the implementation details of where the data comes from.
A repository may coordinate remote APIs, local databases, caches, and other data sources. The exact responsibilities depend on the application's requirements and architecture.
7.1 Why Use a Repository?
- Separates data access from UI and presentation logic.
- Hides implementation details of APIs and databases.
- Makes it easier to change a data source.
- Provides a consistent interface for data operations.
- Improves unit testing by allowing repository implementations to be replaced with fakes or mocks.
- Can coordinate caching, synchronization, and error handling.
7.2 Repository Architecture
View
|
v
ViewModel
|
v
Repository
/ \
v v
Remote API Local Database
\ /
v v
Data Results
For example, a product repository might retrieve the latest product list from an API and expose cached data from a local database. The ViewModel does not need to know the details of the HTTP client or SQL queries.
8. Creating a Repository in Kotlin
Consider an application that displays a list of products. The repository provides a method to retrieve product data, while the ViewModel manages the screen state.
Step 1: Create a Product Model
data class Product(
val id: Int,
val name: String,
val price: Double
)
Step 2: Define a Repository Interface
interface ProductRepository {
suspend fun getProducts(): List<Product>
}
The interface defines the required operation without specifying how products are obtained.
Step 3: Create a Repository Implementation
class FakeProductRepository : ProductRepository {
override suspend fun getProducts(): List<Product> {
return listOf(
Product(1, "Laptop", 55000.0),
Product(2, "Headphones", 2500.0),
Product(3, "Keyboard", 1200.0)
)
}
}
This implementation returns sample data for learning and testing. A production implementation could call a remote API or retrieve records from a local database.
Step 4: Use the Repository in a ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
sealed interface ProductUiState {
data object Loading : ProductUiState
data class Success(
val products: List<Product>
) : ProductUiState
data class Error(
val message: String
) : ProductUiState
}
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _uiState =
MutableStateFlow<ProductUiState>(
ProductUiState.Loading
)
val uiState: StateFlow<ProductUiState> =
_uiState.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_uiState.value = ProductUiState.Loading
try {
val products = repository.getProducts()
_uiState.value =
ProductUiState.Success(products)
} catch (exception: Exception) {
_uiState.value =
ProductUiState.Error(
exception.message
?: "Unable to load products"
)
}
}
}
}
This example uses a sealed interface to represent the screen's loading, success, and error states. The repository is supplied through the constructor, which also makes the ViewModel easier to test.
The sample catches exceptions to demonstrate UI state handling. Production code should handle expected errors deliberately, avoid swallowing coroutine cancellation, and avoid exposing sensitive internal error details directly to users.
9. Repository Pattern with Remote and Local Data Sources
Many real Android applications use more than one data source. For example, a news application may use a remote API for current articles and a Room database for locally cached content.
Example Data Flow
- The View requests article data through the ViewModel.
- The ViewModel calls the repository.
- The repository checks the local data source or retrieves remote data according to the application's policy.
- The repository coordinates storing or refreshing data.
- The ViewModel exposes the result as UI state.
- The View displays the articles and any relevant loading or error information.
Possible Repository Responsibilities
| Responsibility |
Example |
| Remote access |
Fetch articles through a REST API. |
| Local access |
Read cached articles from Room. |
| Caching |
Store successful network results for future access. |
| Synchronization |
Refresh stale data according to a defined policy. |
| Error handling |
Translate data-source failures into application-level results. |
A repository should not become an enormous class that handles every data operation in the application. Separate repositories or dedicated data-source classes can be used when responsibilities become too broad.
10. What Is Dependency Injection?
Dependency Injection (DI) is a design technique in which an object receives the other objects it needs from an external source rather than constructing those objects itself.
A dependency is any object or service that a class needs to perform its work. For example, a ProductViewModel depends on a ProductRepository.
10.1 Without Dependency Injection
class ProductViewModel : ViewModel() {
private val repository =
FakeProductRepository()
}
This ViewModel creates its own repository. Although the example is simple, it becomes difficult to replace the repository with another implementation for testing or production.
10.2 With Constructor Injection
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel()
Now the repository is supplied to the ViewModel from outside. A test can pass a fake repository, while the application can supply a production implementation.
Benefits of Dependency Injection
- Reduces tight coupling between classes.
- Improves unit testing.
- Encourages programming against interfaces.
- Makes dependencies explicit.
- Supports replacing implementations without rewriting consumers.
- Helps manage shared object lifetimes when using an appropriate DI framework.
11. Types of Dependency Injection
11.1 Constructor Injection
Constructor injection supplies dependencies through a class constructor. It is usually the preferred approach for required dependencies because the class cannot be constructed without them.
class UserService(
private val repository: ProductRepository
)
11.2 Setter or Property Injection
Setter injection supplies a dependency through a setter or property after an object has been created. It can be useful for optional dependencies or framework-managed classes, but it may allow an object to exist in an incompletely initialized state.
11.3 Method Injection
Method injection passes a dependency as a method parameter when the dependency is needed only for a particular operation.
class ReportGenerator {
fun generate(repository: ProductRepository) {
// Use the supplied repository.
}
}
Comparison of Injection Types
| Type |
How It Works |
Common Use |
| Constructor injection |
Dependencies are passed during object creation. |
Required dependencies in services, repositories, and ViewModels. |
| Setter/property injection |
Dependencies are assigned after construction. |
Optional dependencies or framework-managed objects. |
| Method injection |
Dependencies are passed to a method call. |
Operations requiring a temporary collaborator. |
12. Dependency Injection Using Hilt
Hilt is a dependency injection library for Android built on Dagger. It helps manage object creation and dependency graphs using annotations and generated code.
Instead of manually constructing every repository and service, developers declare how dependencies should be provided and let Hilt create and connect the required objects.
12.1 Important Hilt Annotations
| Annotation |
Purpose |
| @HiltAndroidApp |
Enables Hilt for the Android application and initializes its component structure. |
| @AndroidEntryPoint |
Enables Hilt injection in supported Android framework classes. |
| @Inject |
Marks constructors or fields for injection. |
| @Module |
Defines a module that contributes dependency bindings. |
| @Provides |
Defines how to create a dependency using a provider function. |
| @Binds |
Maps an implementation to an interface through an abstract binding method. |
| @InstallIn |
Specifies the Hilt component in which a module is installed. |
| @HiltViewModel |
Marks a ViewModel that Hilt should construct. |
| @Singleton |
Scopes a binding to a single instance within the relevant singleton component. |
Hilt annotations are processed by the build system. They do not work simply by adding imports; the appropriate Hilt and Kotlin or Java build configuration must also be installed.
13. Setting Up Hilt in an Android Project
Before using Hilt, configure its Gradle plugin, dependencies, and annotation processing according to the official Android documentation.
Step 1: Configure the Dependencies
Add the Hilt Gradle plugin and runtime dependencies using versions compatible with your Android Gradle Plugin, Kotlin version, and project configuration. A Kotlin DSL dependency example is shown below.
dependencies {
implementation("com.google.dagger:hilt-android:YOUR_HILT_VERSION")
ksp("com.google.dagger:hilt-compiler:YOUR_HILT_VERSION")
}
Replace YOUR_HILT_VERSION with the current compatible Hilt version. This snippet assumes that the Hilt Gradle plugin and KSP plugin have already been configured. Follow the current official setup guide if your project uses a different supported processing configuration.
Step 2: Create the Application Class
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application()
Register this application class in the Android manifest.
<application
android:name=".MyApplication"
... >
</application>
Step 3: Enable Injection in an Android Component
import androidx.activity.ComponentActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity()
Use the appropriate Hilt-supported Android component and dependencies for the Activity, Fragment, or other component in your project.
14. Injecting a Repository with Hilt
When a repository implementation can be constructed through its constructor, use constructor injection to declare its dependencies.
Step 1: Create the Repository Interface
interface ProductRepository {
suspend fun getProducts(): List<Product>
}
Step 2: Implement the Repository
import javax.inject.Inject
class DefaultProductRepository @Inject constructor() :
ProductRepository {
override suspend fun getProducts(): List<Product> {
return listOf(
Product(1, "Laptop", 55000.0),
Product(2, "Mouse", 800.0)
)
}
}
Step 3: Bind the Interface to the Implementation
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindProductRepository(
implementation: DefaultProductRepository
): ProductRepository
}
The binding tells Hilt which implementation to provide when a class requests a ProductRepository. The singleton scope is appropriate here only if the application intends to share one repository instance for the lifetime of the application component.
Step 4: Inject the Repository into a ViewModel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel()
Hilt can now construct ProductViewModel with the configured repository implementation. The ViewModel can use the repository to retrieve data and expose UI state as shown earlier.
15. Understanding Dependency Injection Scopes
Scopes determine how long a provided dependency instance is retained within a dependency injection component.
| Scope |
General Purpose |
| @Singleton |
Shares an instance within the application's SingletonComponent. |
| @ActivityRetainedScoped |
Shares an instance across the lifetime of an Activity's retained component, including configuration changes. |
| @ViewModelScoped |
Shares an instance within the lifetime of a particular Hilt ViewModel component. |
| @ActivityScoped |
Shares an instance within a particular Activity component. |
| @FragmentScoped |
Shares an instance within a particular Fragment component. |
Choose a scope based on the intended lifetime and sharing requirements. A singleton is not automatically the best choice for every dependency because it may retain objects longer than necessary.
16. How MVVM, Repository, and DI Work Together
These concepts solve different architectural problems and are most useful when combined with clear boundaries.
Android UI
(Activity / Fragment / Compose)
|
v
ViewModel
|
v
Repository Interface
|
v
Repository Implementation
/ \
v v
Remote API Local Database
Dependency Injection:
Provides the required implementation
to the ViewModel or other consumers.
Example Workflow
- A user opens the product screen.
- The View collects the ViewModel's UI state.
- The ViewModel requests products from the repository.
- Hilt supplies the configured repository implementation.
- The repository retrieves products from a data source.
- The ViewModel converts the result into a success or error state.
- The View renders the updated state.
In this design, the View does not need to know how products are stored, the ViewModel does not construct its own repository, and the repository does not need to know which screen displays its data.
17. MVVM vs MVC vs MVP
| Feature |
MVC |
MVP |
MVVM |
| Full form |
Model-View-Controller |
Model-View-Presenter |
Model-View-ViewModel |
| Main logic coordinator |
Controller |
Presenter |
ViewModel |
| UI communication |
Depends on implementation |
Presenter often communicates through a View contract |
View observes or collects UI state |
| Testability |
Depends on separation of responsibilities |
Presenter can be tested independently of the UI |
ViewModel and data layers can be tested independently |
| Android use |
Possible with appropriate design |
Possible with a presenter-based architecture |
Commonly used with Android Jetpack components |
No architectural pattern is automatically best for every application. The correct choice depends on the application's complexity, team conventions, testability requirements, and maintenance goals.
18. Repository Pattern vs Direct Data Access
| Aspect |
Direct Data Access |
Repository Pattern |
| Data access |
UI or ViewModel may call a data source directly. |
Consumers access data through a repository abstraction. |
| Coupling |
Can become tightly coupled to the data source. |
Can reduce coupling between consumers and data-source implementations. |
| Testing |
May require more setup for database or network dependencies. |
Allows repository interfaces to be replaced with test doubles. |
| Complexity |
Simple for very small applications. |
Adds an abstraction that is valuable when data logic warrants it. |
| Data coordination |
May be duplicated across consumers. |
Can centralize caching and source-selection policies. |
A repository is not mandatory for every tiny application or every simple data access operation. Introduce it when it provides a meaningful separation of concerns rather than adding layers without a clear purpose.
19. Unit Testing MVVM and Repositories
Testing becomes easier when ViewModels depend on interfaces rather than constructing concrete data sources themselves.
Example Fake Repository
class TestProductRepository : ProductRepository {
override suspend fun getProducts(): List<Product> {
return listOf(
Product(1, "Test Laptop", 1000.0)
)
}
}
A test can provide this fake implementation to the ViewModel and verify that the UI state changes to Success with the expected product.
What Should Be Tested?
- ViewModel state before and after loading.
- Successful repository responses.
- Repository failures and error states.
- Empty result handling.
- Data transformation and validation logic.
- Repository behavior for local and remote data sources.
- Dependency injection configuration for important application bindings.
For coroutine-based tests, use appropriate coroutine testing utilities and control dispatchers so tests remain deterministic. Android UI behavior may also require instrumentation or UI tests.
20. Common Architecture Mistakes
- Putting all logic in an Activity: Makes the UI difficult to test and maintain.
- Making the ViewModel responsible for database queries: Couples presentation logic to a particular data source.
- Creating dependencies inside consumers: Makes replacement and testing harder.
- Using a repository for every trivial operation without a reason: Adds unnecessary abstraction.
- Using overly broad singleton scopes: May keep objects alive longer than necessary.
- Exposing mutable state publicly: Allows consumers to modify state they should only observe.
- Ignoring coroutine cancellation: Can lead to unnecessary work or incorrect lifecycle behavior.
- Returning raw exceptions directly to users: Can expose confusing or sensitive implementation details.
- Making repositories depend on Android UI classes: Weakens separation between the data and presentation layers.
- Assuming ViewModel guarantees permanent persistence: Important state may still need saved state or a database.
21. Practical Project: Product Listing Application
Project objective: Build a simple Android application that displays products using MVVM, a repository abstraction, and dependency injection.
Functional Requirements
- Display a list of products.
- Load product data from a repository.
- Show loading, success, empty, and error states.
- Use a ViewModel to manage UI state.
- Use Hilt to provide the repository dependency.
- Allow the repository implementation to be replaced with a fake during testing.
Suggested Project Structure
com.example.productapp
|
|-- data
| |-- model
| | |-- Product.kt
| |-- repository
| | |-- ProductRepository.kt
| | |-- DefaultProductRepository.kt
|
|-- di
| |-- RepositoryModule.kt
|
|-- ui
| |-- product
| |-- ProductScreen.kt
| |-- ProductViewModel.kt
|
|-- MyApplication.kt
|-- MainActivity.kt
Development Steps
- Create the Android project and configure Kotlin.
- Define the Product data model.
- Create the ProductRepository interface.
- Implement a fake or real repository.
- Create a ViewModel with observable UI state.
- Configure Hilt and bind the repository interface.
- Build the UI to collect state and display products.
- Add loading and error handling.
- Write unit tests using a fake repository.
- Test configuration changes and failure scenarios.
22. Interview Questions and Answers
Q1. What Is MVVM?
Answer: MVVM stands for Model-View-ViewModel. It separates UI rendering, UI-related state, and data or domain responsibilities to improve maintainability and testability.
Q2. What Is the Role of a ViewModel?
Answer: A ViewModel manages UI-related state and presentation logic, coordinates operations, and exposes data that the View can observe. It should avoid retaining direct references to short-lived UI objects.
Q3. What Is the Repository Pattern?
Answer: The Repository Pattern provides an abstraction over data access so that the rest of the application does not need to know the implementation details of databases, network APIs, or caches.
Q4. What Is Dependency Injection?
Answer: Dependency Injection supplies the dependencies required by a class from outside the class, reducing tight coupling and improving testability.
Q5. What Is Constructor Injection?
Answer: Constructor injection passes dependencies through a class's constructor. It is useful for required dependencies because the object cannot be constructed without receiving them.
Q6. What Is Hilt?
Answer: Hilt is an Android dependency injection library built on Dagger. It simplifies dependency graph configuration, object creation, and component scoping.
Q7. Can a ViewModel Survive Screen Rotation?
Answer: An AndroidX ViewModel can retain its state across configuration changes when the same ViewModel owner is retained. Additional mechanisms are needed for restoring state after process death when required.
Q8. Can a Repository Use Multiple Data Sources?
Answer: Yes. A repository can coordinate local storage, a remote API, and caching according to the application's data requirements.
Q9. What Is the Difference Between @Provides and @Binds?
Answer: @Provides defines a function that constructs or returns an object. @Binds declares an abstract mapping from an interface to an implementation that can be supplied by dependency injection.
Q10. Why Is Constructor Injection Helpful in Unit Testing?
Answer: It allows tests to provide fake or mock dependencies directly, making it easier to test a class without relying on actual network services or databases.
Q11. What Is a StateFlow?
Answer: StateFlow is a Kotlin Flow type that holds a current state value and emits updates to its collectors. It is often used to expose UI state from a ViewModel.
Q12. Does MVVM Require Hilt?
Answer: No. MVVM can be implemented with manual dependency creation or other dependency injection approaches. Hilt is an optional tool that helps manage dependencies in Android projects.
Q13. Is a Repository the Same as a DAO?
Answer: No. A DAO generally defines operations for accessing a database, while a repository provides a broader data-access abstraction and may coordinate multiple data sources or business-specific data policies.
Q14. What Is the Benefit of UI State Modeling?
Answer: Representing loading, success, empty, and error states explicitly makes UI behavior easier to reason about, test, and maintain.
Q15. Should a ViewModel Hold an Activity Context?
Answer: A ViewModel should generally avoid holding Activity or View references because their lifetimes may be shorter than the ViewModel's. Application-scoped dependencies may be appropriate for particular requirements, but should be used carefully.
23. Best Practices for Android Architecture
- Keep Activities and Fragments focused on UI responsibilities.
- Use ViewModels to manage UI-related state.
- Expose immutable state to the UI.
- Use repository interfaces when they provide a useful abstraction.
- Prefer constructor injection for required dependencies.
- Use Hilt or another suitable dependency management approach consistently.
- Choose dependency scopes based on object lifetimes.
- Keep data-source implementation details out of presentation logic.
- Handle errors and coroutine cancellation correctly.
- Test ViewModels and repositories independently where practical.
- Use clear package structures and meaningful class names.
- Avoid adding architectural layers that do not solve a real problem.
24. Quick Revision Checklist
- Understand the roles of Model, View, and ViewModel.
- Know how a ViewModel exposes UI state.
- Understand repository interfaces and implementations.
- Know how repositories coordinate APIs and local databases.
- Understand constructor, property, and method injection.
- Know the purpose of Hilt annotations.
- Understand dependency scopes and object lifetimes.
- Know how MVVM, repositories, and DI work together.
- Understand how fake repositories support unit testing.
- Recognize common lifecycle, state, and dependency management mistakes.
25. Conclusion
MVVM, the Repository Pattern, and Dependency Injection are important concepts for building maintainable Android applications. MVVM organizes presentation logic, repositories separate data access from consumers, and dependency injection supplies the objects required by each component.
When these concepts are used thoughtfully, developers can create applications that are easier to test, debug, extend, and maintain. However, architecture should match the complexity of the application, and additional abstractions should be introduced when they provide clear value.
Continue learning through the JustAcademy Android App Development Course, visit the course demo registration page, and explore the JustAcademy website for further learning resources.
End of Notes: Explaining MVVM, Repository Patterns, and Dependency Injection